/*******************************************************************************
 * Copyright (c) 2014 Steffen Mikkelsen
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl.html
 * 
 * Contributors:
 *     Steffen Mikkelsen - initial API and implementation
 ******************************************************************************/
package com.subsecret.secureconnection.client;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

import com.subsecret.secureconnection.securestream.SecureStream;
import com.subsecret.secureconnection.securestream.SecureStreamLayer;
import com.subsecret.secureconnection.securestream.layers.aes.AESEncryptionLayer;
import com.subsecret.secureconnection.securestream.layers.gzip.GZIPCompressionLayer;
import com.subsecret.secureconnection.securestream.layers.obfuscation.ObfuscationLayer;
import com.subsecret.secureconnection.streamtools.ForwardDataThread;
import com.subsecret.secureconnection.streamtools.StreamConstants;
import com.subsecret.secureconnection.streamtools.StreamTools;

public class StreamForwardClient {
    
    private final ClientConnInfo ci;
    
    public StreamForwardClient(ClientConnInfo ci) {
        this.ci = ci;
    }
    
    public void start() throws IOException {
        ArrayList<SecureStreamLayer> layers = new ArrayList<>();
        layers.add(new GZIPCompressionLayer());
        layers.add(new ObfuscationLayer());
        layers.add(new AESEncryptionLayer());
        
        ServerSocket ssock = new ServerSocket(ci.localport);
        int connNumber = 0;
        while (!ssock.isClosed()) {
            try {
                connNumber++;
                Socket lsock = ssock.accept();
                new ConnectionHandler(lsock, layers, ci, connNumber).start();
                
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
        ssock.close();
    }
    
    public static class ConnectionHandler extends Thread {
        private final Socket socket;
        private final ArrayList<SecureStreamLayer> layers;
        private final ClientConnInfo ci;
        private final int connNumber;
        
        public ConnectionHandler(Socket sock, ArrayList<SecureStreamLayer> layers, ClientConnInfo ci, int connNumber) {
            this.socket = sock;
            this.layers = layers;
            this.ci = ci;
            this.connNumber = connNumber;
        }
        
        @Override
        public void run() {
            try {
                
                // Connect to server
                Socket remSock = new Socket(ci.serverHost, ci.serverPort);
                InputStream serverIn = remSock.getInputStream();
                OutputStream serverOut = remSock.getOutputStream();
                
                final SecureStream createSecureStreamClient = SecureStream.createSecureStreamClient(serverIn, serverOut, true,
                        layers.toArray(new SecureStreamLayer[0]));
                
                serverIn = createSecureStreamClient.getInputStream();
                serverOut = createSecureStreamClient.getOutputStream();
                StreamTools.sendString(serverOut, ci.password);
                byte ans = StreamTools.receiveByte(serverIn);
                if (ans != StreamConstants.PASSWORD_OK) {
                    // failed. abort
                    remSock.close();
                    socket.close();
                    throw new IOException("Bad password");
                }
                
                StreamTools.sendString(serverOut, ci.remoteHost);
                StreamTools.sendInteger(serverOut, ci.remotePort);
                byte connStatus = StreamTools.receiveByte(serverIn);
                if (connStatus == StreamConstants.CONN_FAIL) {
                    remSock.close();
                    socket.close();
                    throw new IOException("App connection on serverside failed");
                }
                
                // Multiplexing logic can be added here
                
                InputStream localIn = socket.getInputStream();
                OutputStream localOut = socket.getOutputStream();
                final ForwardDataThread f1 = new ForwardDataThread(localIn, serverOut);
                f1.start();
                final ForwardDataThread f2 = new ForwardDataThread(serverIn, localOut);
                f2.start();
                
                StreamTools.startMonitoringStream("Client", connNumber, socket.getLocalPort(), ci, createSecureStreamClient);
                try {
                    f1.join();
                    f2.join();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
                remSock.close();
                socket.close();
            }
            catch (IOException e) {
                e.printStackTrace();
            }
            
        }
    }
}
